"""Provide support for mining a Fido CSV phone record to help choose a cell
phone plan.

All information needed to evaluate a month's worth of calls in terms of helping
to choose a phone plan *should* be provided.

XXX:
    + care about voicemail minutes in tallies?
        - YES!  Calls count as minute usage when connecting.
    + Fido-to-Fido support
        - way to automate phone number lookup as Fido number?
    + How do you detect International long distance?
    + Detecting calls made/received on expanded network?

    + detecting bill date

    + object that allows specifying types of plans to help choose one


"""
import csv
import datetime
import time
from decimal import Decimal as D

# Call types.
INCOMING = "INCOMING"
OUTGOING = "OUTGOING"
VOICEMAIL = "Voice Mail"

# Location types.
LOCAL = "local"
LONG_DISTANCE = "long distance"
INTERNATIONAL = "international"

# Dialed number types; local defined above.
EXPANDED = "expanded"
ROAMING = "roaming"


#XXX Needed?
CANADIAN_PROVINCES = ('AB', 'BC', 'MB', 'NB', 'NL', 'NT', 'NS', 'NU', 'ON',
                        'PE', 'QC', 'SK', 'YT')

class PhoneCall(object):

    """Represent a phone call.

    All data that can be deduced based on the call data  and does not change
    regardless of plan options is represented as an attribute on the object.
    All other information is provided through methods.

    This object can have the attributes of:
        + log_number
            The call number.
        + date
            Date of call.
        + start_time
            When the call started.
        + call_type
            INCOMING, OUTGOING, or VOICEMAIL.
        + my_location
            Where I was when the call was placed/received.
        + location_type
            Whether the cell phone was in a local, expanded, or roaming area.
        + called_number [optional]
            If I placed a call, the number I dialed.
        + called_location [optional]
            Geographic location of the person I called.
        + dialed_type
            Whether dialed number was local, long distance, or international.
        + call_length
            Length of call.
        + cost_min
            Cost per minute.
        + cost_airtime
            Cost of airtime.
        + cost_long_dist
            Long distance cost.
        + cost_total
            Total cost of call.

    """

    def __init__(self, csv_line, year=datetime.date.today().year):
        """Initialise from a parsed line from a CSV file, optionally specifying
        the year of the bill.

        Fido CVS files have *two* line formats.  The one used for the first
        part of the bill is:
            [0] log number
            [1] date of call ("%a %b %d")
            [2] time of call ("%H:%M")
            [3] call from (CITY PROVINCE)
            [4] number called (###-###-####)
            [5] location called (CITY PROVINCE)
            [6] rate period (57, EW)
            [7] type of call (INC, IRM, OUT, ROM, VM)
            [8] call length (%H:%M)
            [9] $/min (decimal)
            [10] airtime charges (decimal)
            [11] long distance charges (decimal)
            [12] total charges (decimal)

        The second part of the bill differs as so:
            [5] city that dialed number is in
            [6] province that dialed number is in
            [7] empty

        There is also a difference with the location of where a call was made
        from.  If it was made while roaming, the province/state is listed at
        the beginning *and* end of the entry.

        """
        self.log_number = int(csv_line[0])
        time_tuple = time.strptime(csv_line[1], "%a %b %d")
        self.date = datetime.date(year, time_tuple.tm_mon, time_tuple.tm_mday)
        self.start_time = datetime.time(*[int(num)
                                        for num in csv_line[2].split(':')])
        # Classify call.
        if csv_line[3] == INCOMING:
            self.call_type = INCOMING
        else:
            location_parts = tuple(csv_line[3].split())
            if len(location_parts) == 2:
                self.my_location = location_parts
            elif (len(location_parts) == 3 and
                    location_parts[0] == location_parts[2]):
                self.my_location = location_parts[1:]
            else:
                raise ValueError("cannot parse '%s'" % csv_line[3])
            if csv_line[4] == VOICEMAIL:
                self.call_type = VOICEMAIL
            else:
                self.call_type = OUTGOING
                self.called_number = tuple(csv_line[4].split('-'))

        # Handle difference between the two line formats.
        if csv_line[7]:
            location = tuple(csv_line[5].split())
        else:
            location = (csv_line[5], csv_line[6])
        
        if self.call_type == INCOMING:
            self.my_location = location
        else:
            self.called_location = location

        # Call length.
        call_min, call_sec = (int(num) for num in csv_line[8].split(':'))
        self.call_length = (call_min * 60) + call_sec
        # Dollar values.
        self.cost_min = D(csv_line[9])
        self.cost_airtime = D(csv_line[10])
        self.cost_long_dist = D(csv_line[11])
        self.cost_total = D(csv_line[12])

        # Location type.
        # XXX Handle expanded coverage.
        self.location_type = LOCAL if self.cost_min != D("0.50") else ROAMING

        # Dialed number type.
        # XXX Handle International calls.
        self.dialed_type = (LOCAL if self.cost_long_dist == 0
                                else LONG_DISTANCE)

    def evening(self):
        """Return True if call was made during the evening on a weekday, with
        the option specifying when the evening starts."""
        evening_starts = datetime.time(19)
        day_starts = datetime.time(8)
        # Monday: evenings
        if self.date.weekday() == 0:
            if self.start_time > evening_starts:
                return True
            else:
                return False
        # Tuesday, Wednesday, Thursday: mornings and evenings
        elif self.date.weekday() in (1, 2, 3):
            if (self.start_time < day_starts or
                    self.start_time > evening_starts):
                return True
            else:
                return False
        # Friday: mornings
        elif self.date.weekday() == 4:
            if self.start_time < day_starts:
                return True
            else:
                return False
        # Saturday, Sunday: never
        else:
            return False
        
    def weekend(self):
        """Return True if call was made during the weekend."""
        weekend_hour_starts = datetime.time(19)
        weekend_hour_ends = datetime.time(8)
        # Monday
        if self.date.weekday() == 0:
            if self.start_time < weekend_hour_ends:
                return True
            else:
                return False
        # Tuesday, Wednesday, Thursday
        elif self.date.weekday() in (1, 2, 3):
            return False
        # Friday
        elif self.date.weekday() == 4:
            if self.start_time > weekend_hour_starts:
                return True
            else:
                return False
        # Saturday, Sunday
        else:
            return True

    def weekday(self):
        """Return True if the call was made during the day during the week."""
        return not (self.evening() or self.weekend())


class MonthlyUsage(object):

    """Represent a month's worth of calls as extracted from a bill."""

    def __init__(self, csv_path):
        """Initialise based on information gathered from the specified path to
        a CSV file."""
        #XXX Detect bill info (date, phone number, ID, etc.)
        call_records = []
        self.weekday_minutes = [0, 0]
        self.evening_minutes = [0 ,0]
        self.weekend_minutes = [0, 0]
        self.long_distance_minutes = 0
        csv_FILE = open(csv_path, 'rb')
        try:
            for csv_line in csv.reader(csv_FILE):
                if csv_line and csv_line[0] and len(csv_line) == 13:
                    try:
                        int(csv_line[0])
                    except ValueError:
                        continue
                    else:
                        call = PhoneCall(csv_line)
                        call_records.append(PhoneCall(csv_line))
                        minutes = call.call_length / 60.
                        if call.weekday():
                            if call.call_type == INCOMING:
                                self.weekday_minutes[0] += minutes
                            else:
                                self.weekday_minutes[1] += minutes
                        elif call.evening():
                            if call.call_type == INCOMING:
                                self.evening_minutes[0] += minutes
                            else:
                                self.evening_minutes[1] += minutes
                        else:
                            if call.call_type == INCOMING:
                                self.weekend_minutes[0] += minutes
                            else:
                                self.weekend_minutes[1] += minutes

                        if (call.call_type == OUTGOING and
                                call.dialed_type == LONG_DISTANCE):
                            self.long_distance_minutes += minutes
        finally:
            csv_FILE.close()
        self.calls = call_records


def print_info(csv_path):
    """Print out calling information for bill in a format that makes it easy to
    help choose a cell phone plan."""
    monthly_usage = MonthlyUsage(csv_path)
    print "weekday minutes =", sum(monthly_usage.weekday_minutes)
    print "\tincoming =", monthly_usage.weekday_minutes[0]
    print "\toutgoing =", monthly_usage.weekday_minutes[1]
    print "evening minutes =", sum(monthly_usage.evening_minutes)
    print "\tincoming =", monthly_usage.evening_minutes[0]
    print "\toutoing =", monthly_usage.evening_minutes[1]
    print "weekend minutes =", sum(monthly_usage.weekend_minutes)
    print "\tincoming =", monthly_usage.weekend_minutes[0]
    print "\toutgoing =", monthly_usage.weekend_minutes[1]
    print
    print "total =", sum(call.call_length
                            for call in monthly_usage.calls) / 60.
    print
    print "EXTRAS ----------------"
    print "long distance minutes =", monthly_usage.long_distance_minutes
    print "happy hour minutes =", sum(call.call_length
            for call in monthly_usage.calls
                if call.start_time > datetime.time(17) and
                    call.start_time < datetime.time(19)) / 60.


if __name__ == '__main__':
    import sys

    if len(sys.argv) > 2:
        raise ValueError("only one argument accepted")

    print_info(sys.argv[1])
